Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API for runtime parameters, e.g. for configuring federation upstream components. #150

Merged
merged 19 commits into from
May 6, 2020

Conversation

niclic
Copy link
Contributor

@niclic niclic commented Apr 26, 2020

What this PR does:

  • Adds additional client methods to allow reading of federation upstream parameters.
  • Adds tests for all existing and new federation methods.

Why do we need this PR?

  • To add federation support to the terraform-provider-rabbitmq terraform provider, we need to be able to read federation upstream resources via the API. I'm working on this now but need these changes before I can continue.

Notes

Some notes from working on this PR.

API changes

To support the new read methods, I added additional fields to FederationUpstream and introduced a FederationDefinitionDTO type. This is an exact copy of the existing shovel implementation, which makes sense, because they are more or less the same thing.

Default values

The only required field for a federation upstream definition is Uri. Although many of the other arguments have stated defaults in the docs, when a value is not provided, it gets the default value for the go type instead.

For example:

// exported configuration for an upstream with only uri provided
{
  "rabbit_version": "3.8.3",
  "parameters": [
    {
      "value": {
        "exchange": "",
        "expires": 0,
        "max-hops": 0,
        "message-ttl": 0,
        "prefetch-count": 0,
        "queue": "",
        "reconnect-delay": 0,
        "trust-user-id": false,
        "uri": "amqp://127.0.0.1/%2f"
      },
      "vhost": "rabbit/hole",
      "component": "federation-upstream",
      "name": "temporary"
    }
  ]
}

Is the value for prefetch-count the default of 1000, or 0? I don't think these values are the same, although I haven't tested this to be honest. It is easy to supply the default values in a request, which is probably the right thing to do. However, some arguments, such as expires, use a default value of none (according to the docs) or leave blank (in the Admin UI) to mean never expires. Does 0 accomplish the same thing?

Might it be a good idea to annotate these fields with omitempty instead?

Deleting a federation upstream parameter

Deleting a non-existent upstream returns a HTTP 404 response and not an error.

curl -i -u guest:guest \
-X DELETE http://localhost:15672/api/parameters/federation-upstream/rabbit%2fhole/temporary

HTTP/1.1 404 Not Found
content-length: 49
content-security-policy: script-src 'self' 'unsafe-eval' 'unsafe-inline'; object-src 'self'
content-type: application/json
date: Sun, 26 Apr 2020 14:38:08 GMT
server: Cowboy
vary: accept, accept-encoding, origin

{"error":"Object Not Found","reason":"Not Found"}

Same for PutFederationUpstream. Both methods use the executeResponse function, which does not parse the response into an ErrorResponse{} error.

This is probably intentional, but I've added tests for these cases in case this logic is ever changed.

Additional federation arguments

  • The federation reference docs show an additional federated queue argument called consumer-tag.

  • The Admin > Federation Upstreams > Add a new upstream UI shows an additional federated exchange argument called ha-policy.

  • The federation plugin source shows some additional arguments that may not be documented yet: resource-cleanup-mode and bind-nowait.

Perhaps the API should support these additional arguments as well?

  -- note that doc.go is out of sync.
Copy link
Owner

@michaelklishin michaelklishin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. This looks like a good addition but this specific revision introduces incorrect terminology that I cannot accept.

Specifically it confuses runtime parameters — one of the ways to configure stuff on a RabbitMQ node — with federation upstreams.

Federation upstreams are defined using runtime parameters. A federation upstream is a component (a type of runtime parameter) but there are other parameters (policies and dynamic Shovels are two examples), and more
are coming in future RabbitMQ versions.
In fact, you can see that the endpoint used starts with {prefix}/parameters.

It's perfectly reasonable to have functions that specifically operate on federation upstreams
but we should keep the distinction clear. The good news is that with a few changes this PR would cover more ground than you've originally planned to.

I will leave a few specific comments in the diff.

federation.go Outdated
//

// ListFederationUpstreams returns all federation upstreams
func (c *Client) ListFederationUpstreams() (rec []FederationUpstream, err error) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be very nice to accompany this (and, perhaps, base it on) with a set of functions that would list, set, and delete arbitrary runtime parameters. Note that RabbitMQ nodes will perform parameter validation so if a component is not known (not registered by a plugin or RabbitMQ core), it will be rejected.

We can also add functions that work with parameters of a certain type. Then federation upstream management would rely on them and pass federation-upstream for component. How does this sound?

@michaelklishin
Copy link
Owner

Note that the suggested API changes would not require a lot of new tests. An absolute minimum would do, and the existing federation upstream tests would provide most coverage.

@niclic
Copy link
Contributor Author

niclic commented May 3, 2020

Sounds good. When I first came to adding support for managing federation upstreams to terraform, I expected to be defining parameter types and resources. However, I was swayed by the existence of both Federation and Shovel types already present in rabbithole. I probably should have opened a discussion beforehand. I think adding a set of methods that operate explicitly on runtime parameters aligns well with the underlying RabbitMQ APIs.

The docs describe using vhost-scoped parameters to configure settings at run time. I use your suggested name of RuntimeParameter.

A possible API design for managing runtime parameters might look like this.

This mirrors the underlying HTTP API (except for the order of path params) and follows existing rabbithole API conventions. The corresponding HTTP API calls are shown in the comments.

This is very much influenced by the Federation and Shovel plugins, but perhaps there are other things to consider? There are already methods for managing Shovels.

type RuntimeParameter struct {
  Name      string                `json:"name"`
  Vhost     string                `json:"vhost"`
  Component string                `json:"component"`
  Value     RuntimeParameterValue `json:"value"`
}

// could also just use the go type directly or interface{}
type RuntimeParameterValue map[string]interface{}


// /api/parameters A list of all vhost-scoped parameters.
ListRuntimeParameters()
  => []RuntimeParameter, error

// /api/parameters/component   A list of all vhost-scoped parameters for a given component.
ListRuntimeParametersFor(component string)
  => []RuntimeParameter, error

ListRuntimeParametersIn(vhost, component string)
  => []RuntimeParameter, error

// /api/parameters/component/vhost/name  An individual vhost-scoped parameter.
GetRuntimeParameter(vhost, component, name string)
  => *RuntimeParameter, error

PutRuntimeParameter(vhost, component, name string, value RuntimeParameterValue)
  => error

DeleteRuntimeParameter(vhost, component, name string)
  => error

Thoughts?

@niclic
Copy link
Contributor Author

niclic commented May 3, 2020

The above API were shown as public methods. But separate API for managing runtime parameters and federation upstream might be redundant or confusing.

I think this is what you were getting at. I got it.

For example.

// federation.go
func (c *Client) GetFederationUpstream(vhost, upstreamName string) (param *RuntimeParameter, err error) {
    return getRuntimeParameter(vhost, "federation-upstream", &param)
}

// runtime_parameter.go
func getRuntimeParameter(vhost, name string, rec interface{}) err error {
  // ...
  executeAndParseRequest(c, req, &rec)
  // ...
}

Let me put something together for review.

@michaelklishin
Copy link
Owner

The proposed API makes sense to me. Additional public functions that operate specifically on upstreams also make sense to me. Your example with GetFederationUpstream is indeed what I had in mind but getRuntimeParameter can be GetRuntimeParameter instead (so, public). Managing runtime parameters is a thing that operators might want to do and automate :)

@michaelklishin
Copy link
Owner

Thank you for your willingness to invest more time into this PR!

@niclic
Copy link
Contributor Author

niclic commented May 5, 2020

Okay, please review these changes.

This is still WIP, but I wanted to check in before moving completing the changes.

Changes so far

  1. Added an API for runtime (vhost-scoped) parameters. See this commit.

This allows you to work with parameter values containing arbitrary data. See this test.
You can use this API to manage runtime parameters directly and forget about the other APIs. But you will have to work directly with map[string]interface{}.

  1. Added some V2 methods to federation.go for PUTting and GETting runtime parameters that have a component of federation-upstream.

I have only added a couple of temporary V2 methods to show one approach to using the runtime parameter API to manage different types of runtime parameters.

  1. To avoid any naming confusion, I renamed FederationUpstream to FederationInfo.

This approach requires a struct to represent a strongly-typed parameter value and this mirrors the approach used for Shovels. Note the PopulateRuntimeParameter method. We need to pass the desired struct to the JSON decoder. Otherwise, you will have to do some form of mapping. This method was private to begin with, but I made it public.

Other approaches worth considering.

  1. The parameter API could just return RuntimeParameter and the federation API would be responsble for mapping from map[string]interface{} to FederationInfo.

This is a common approach, but requires extra code to safely access map values. However, it would remove the need for the PopulateRuntimeParameter method, making the API more consistent. Seems like a good option.

This sort of thing.

  // inside GetFederationUpstream
  param, err := GetRuntimeParameter()

  info := &FederationInfo{
    Name: param.Name,
    Definition: FederationDefinition{} 
  }

  m := param.Value.(map[string]interface{})
  if v, ok := m["uri"].(string); ok {
    info.Definition.Uri = v
  }

  return info
  // etc
  1. I don't think returning RuntimeParameter from the federation API itself is an option, since the end user would need to do some some of conversion.

  2. However, in addition to 1, you could embed RuntimeParamter in FederationInfo. This would help to cement the use of runtime parameters for configuring federation, but is really only a semantic change. This uses field shadowing to ignore the embedded RuntimeParameter.Value field. I would be concerned that this behaviour could change at some point in the future though, but shadowing of nested fields is supported.

type FederationInfo struct {
  RuntimeParameter
  Value FederationDefinition `json:"value"`
}
// decodes to the following json
// {
//   "name": "temporary",
//   "vhost": "rabbit/hole",
//   "component": "federation-upstream",
//   "value": {
//     "uri": "amqp://127.0.0.1/%2f",
//     "expires": 0,
//     "message-ttl": 0,
//     "max-hops": 0,
//     "prefetch-count": 1000,
//     "reconnect-delay": 1,
//     "ack-mode": "on-confirm",
//     "trust-user-id": false,
//     "exchange": "",
//     "queue": ""
//   }
// }

There is probably a more elegant solution to all of this, but this seems pretty consistent with the overall API now.

Thoughts?

@niclic niclic changed the title Add methods to read federation upstream parameters. Add API methods to read runtime parameters, e.g. for configuring federation upstream components. May 5, 2020
@michaelklishin
Copy link
Owner

This looks close to what I had in mind. Constructing a federation upstream (info if you will) struct from a runtime parameter struct makes most sense to me. Embedding the parameter feels like a loss of "encapsulation" for no particular benefit.

I personally think that FederationUpstream is the best name. "Federation info" is not very specific since there are like three "parts" that make up a federated entity: an upstream, a link, and the entity itself (some may even add a policy), and the struct clearly only covers the first part.

Thanks 🙏

@niclic niclic changed the title Add API methods to read runtime parameters, e.g. for configuring federation upstream components. API for runtime parameters, e.g. for configuring federation upstream components. May 6, 2020
@michaelklishin michaelklishin merged commit f408e4a into michaelklishin:master May 6, 2020
@michaelklishin
Copy link
Owner

Thank you so much! This is a great addition to the library.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants